筆記目錄

Skip to content

淺談 C# Property (屬性) 語法糖與 NRT 機制演進

TLDR

  • 屬性初始值設定項(Property Initializers)與 Expression-bodied 屬性在生命週期上不同,前者僅在物件初始化時執行一次,後者每次呼叫均會重新執行,誤用會導致邏輯錯誤。
  • C# 14 引入的 field 關鍵字,解決了自動屬性需為了簡單邏輯(如 Trim())而退回手寫 Backing Field 的痛點。
  • NRT (Nullable Reference Types) 機制需搭配 <WarningsAsErrors>nullable</WarningsAsErrors> 才能發揮強制約束效果。
  • requiredinit 語法解決了 DTO 在 NRT 機制下必須撰寫無意義 null! 預設值的問題。
  • 使用 [SetsRequiredMembers] 可解決參數化建構子與 required 屬性之間的編譯衝突。

C# Property (屬性) 的語法演進與踩雷紀錄

C# 的屬性演進旨在簡化定義,但不同語法在執行時機上有顯著差異,若未釐清極易引發 Bug。

屬性初始值與 Expression-bodied 的區別

什麼情況下會遇到這個問題:在定義唯讀屬性或設定初始值時,混淆了「靜態快取」與「動態計算」的語法。

  • public string Name => "Default" (Expression-bodied):每次被呼叫時都會重新執行。
  • public string Name { get; } = "Default" (Property Initializer):僅在物件實體化(new)時執行一次。

WARNING

災難情境: 若使用 public Guid OrderId => Guid.NewGuid();,每次讀取該屬性都會產生全新的 Guid,導致序列化或 Log 追蹤失效。應改用 public Guid CorrectOrderId { get; } = Guid.NewGuid(); 以確保狀態恆定。

使用 field 關鍵字簡化屬性邏輯

什麼情況下會遇到這個問題:當自動屬性需要加入簡單邏輯(如 Trim()NotifyPropertyChanged),卻不想手寫冗長的 Backing Field 時。

C# 14 引入的 field 關鍵字允許直接存取編譯器產生的隱含欄位:

csharp
public class User {
    public string Name { 
        get;
        set => field = value.Trim();
    }
}

TIP

建議將邏輯處理放在 set 中,避免 get 頻繁呼叫帶來的額外開銷,並減少 Entity Framework Core 等框架直接存取 Backing Field 時可能產生的副作用。

NRT (Nullable Reference Types) 與檢查機制的補全

NRT 的核心在於透過 ? 標註明確宣告參考型別的空值可能性。若要強制執行合約,應在專案檔中設定:

xml
<WarningsAsErrors>nullable</WarningsAsErrors>

解決 DTO 在 NRT 下的初始化困境

什麼情況下會遇到這個問題:在 C# 8.0 至 10.0 時期,非 Null 屬性必須初始化,導致 DTO 必須撰寫 null! 或空字串來騙過編譯器,造成程式碼雜訊。

透過 initrequired 關鍵字,可以確保屬性在初始化後不可變,且強制呼叫端必須給值,無需再寫無意義的預設值:

csharp
public class UserDto {
    public required string UserName { get; init; }
}

// 呼叫端必須給值,否則編譯失敗
UserDto dto = new() { UserName = "Alice" };

處理建構子與 required 的衝突

什麼情況下會遇到這個問題:當類別同時定義了參數化建構子與 required 屬性時,編譯器會因無法透過 { } 初始化而發出警告。

使用 [SetsRequiredMembers] 屬性可告知編譯器該建構子已完成所有必要欄位的賦值:

csharp
using System.Diagnostics.CodeAnalysis;

public class User {
    public required string UserName { get; init; }

    [SetsRequiredMembers]
    public User(string userName) {
        UserName = userName; 
    }
}

required 在 Web API 的應用

在 ASP.NET Core 的 [FromBody] 序列化中,若屬性標記為 required,當前端漏傳該欄位時,系統會自動拋出 JsonException,這有助於明確區分「傳入預設值」與「未傳入欄位」的語意差異。

異動歷程

  • 2026-03-30 初版文件建立。